package main

import (
	"fmt"
	"io/fs"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/antonmedv/clipboard"
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/sahilm/fuzzy"
)

type position struct {
	col, row int
	offset   int
}

type toDelete struct {
	path string
	at   time.Time
}

type walker_model struct {
	path              string              // Current dir path we are looking at.
	files             []fs.DirEntry       // Files we are looking at.
	err               error               // Error while listing files.
	c, r              int                 // Selector position in columns and rows.
	columns, rows     int                 // Displayed amount of rows and columns.
	width, height     int                 // Terminal size.
	offset            int                 // Scroll position.
	positions         map[string]position // Map of cursor positions per path.
	search            string              // Type to select files with this value.
	searchMode        bool                // Whether type-to-select is active.
	searchId          int                 // Search id to indicate what search we are currently on.
	matchedIndexes    []int               // List of char found indexes.
	prevName          string              // Base name of previous directory before "up".
	findPrevName      bool                // On View(), set c&r to point to prevName.
	exitCode          int                 // Exit code.
	previewMode       bool                // Whether preview is active.
	previewContent    string              // Content of preview.
	deleteCurrentFile bool                // Whether to delete current file.
	toBeDeleted       []toDelete          // Map of files to be deleted.
	yankSuccess       bool                // Show yank info
	hideHidden        bool                // Hide hidden files
}

// / ------------------------------------------------- methods for walker model
// Init return a nil tea.Cmd
// code review ok
func (wm *walker_model) Init() tea.Cmd {
	return nil
}

// / handle keyboard event helper
type (
	clearSearchMsg int
	toBeDeleteMsg  int
)

// code review ok
func (wm *walker_model) updateSearch(msg tea.KeyMsg) {
	wm.search += string(msg.Runes)
	names := make([]string, len(wm.files))

	for i, fi := range wm.files {
		names[i] = fi.Name()
	}

	matches := fuzzy.Find(wm.search, names)
	if len(matches) > 0 {
		wm.matchedIndexes = matches[0].MatchedIndexes
		index := matches[0].Index
		wm.c = index / wm.rows
		wm.r = index % wm.rows
	}

	wm.updateOffset()
	wm.saveCursorPosition()
}

// / handle keyboard event
// code review ok
func (wm *walker_model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	// ------------------------------------------- case 1
	case tea.WindowSizeMsg:
		wm.width = msg.Width
		wm.height = msg.Height
		if wm.height < 3 {
			wm.height = 3
		}

		// reset position history as col & row changes
		wm.positions = make(map[string]position)
		// key cursor at same place
		filename, ok := wm.fileName()
		if ok {
			wm.prevName = filename
			wm.findPrevName = true
		}

		// Also, wm.c & wm.r no longer point to the correct indexes
		wm.c = 0
		wm.r = 0

		return wm, nil
	// ---------------------------------------------- case 2
	case tea.KeyMsg:
		// make undo work even if we are in fuzzy mode
		if key.Matches(msg, keyUndo) && len(wm.toBeDeleted) > 0 {
			wm.toBeDeleted = wm.toBeDeleted[:len(wm.toBeDeleted)-1]
			wm.list()
			wm.previewContent = ""
			return wm, nil
		}

		if fuzzyByDefault {
			if key.Matches(msg, keyBack) {
				if len(wm.search) > 0 {
					wm.search = wm.search[:strlen(wm.search)-1]
					return wm, nil
				}
			} else if msg.Type == tea.KeyRunes {
				wm.updateSearch(msg)
				wm.searchId++
				searchId := wm.searchId
				return wm, tea.Tick(2*time.Second, func(time.Time) tea.Msg {
					return clearSearchMsg(searchId)
				})
			}
		} else if wm.searchMode {
			if key.Matches(msg, keySearch) {
				wm.searchMode = false
				return wm, nil
			} else if key.Matches(msg, keyBack) {
				if len(wm.search) > 0 {
					wm.search = wm.search[:strlen(wm.search)-1]
				} else {
					wm.searchMode = false
				}

				return wm, nil
			} else if msg.Type == tea.KeyRunes {
				wm.updateSearch(msg)
				return wm, nil
			}
		}

		// -------------------------------------------- case 2 internal switch for keyprese
		switch {
		case key.Matches(msg, keyForceQuit): // ctrl c
			_, _ = fmt.Fprintln(os.Stderr)
			wm.exitCode = 2
			wm.dontDoPendingDeletions()
			return wm, tea.Quit
		case key.Matches(msg, keyQuit, keyQuitQ): // esc q
			_, _ = fmt.Fprintln(os.Stderr)
			fmt.Println(wm.path)
			wm.exitCode = 0
			wm.performPendingDeletions()
			return wm, tea.Quit
		case key.Matches(msg, keyOpen):
			wm.search = ""
			wm.searchMode = false
			filepath, ok := wm.filePath()
			if !ok {
				return wm, nil
			}
			if fi := fileInfo(filepath); fi.IsDir() {
				wm.path = filepath
				if p, ok := wm.positions[wm.path]; ok {
					wm.c = p.col
					wm.r = p.row
					wm.offset = p.offset
				} else {
					wm.c = 0
					wm.r = 0
					wm.offset = 0
				}

				wm.list()
			} else {
				return wm, wm.openEditor()
			}

		case key.Matches(msg, keyBack): // backspace
			wm.search = ""
			wm.searchMode = false
			wm.prevName = filepath.Base(wm.path)
			wm.path = filepath.Join(wm.path, "..")
			if p, ok := wm.positions[wm.path]; ok {
				wm.c = p.col
				wm.r = p.row
				wm.offset = p.offset
			} else {
				wm.findPrevName = true
			}
			wm.list()

			return wm, nil

		case key.Matches(msg, keyUp):
			wm.moveUp()
		case key.Matches(msg, keyTop, keyPageUp, keyVimTop):
			wm.moveTop()
		case key.Matches(msg, keyBottom, keyPageDown, keyVimBottom):
			wm.moveBottom()
		case key.Matches(msg, keyLeftmost):
			wm.moveLeftmost()
		case key.Matches(msg, keyRightmost):
			wm.moveRightmost()
		case key.Matches(msg, keyHome):
			wm.moveStart()
		case key.Matches(msg, keyEnd):
			wm.moveEnd()
		case key.Matches(msg, keyVimUp):
			wm.moveUp()
		case key.Matches(msg, keyDown):
			wm.moveDown()
		case key.Matches(msg, keyVimDown):
			wm.moveDown()
		case key.Matches(msg, keyLeft):
			wm.moveLeft()
		case key.Matches(msg, keyVimLeft):
			wm.moveLeft()
		case key.Matches(msg, keyRight):
			wm.moveRight()
		case key.Matches(msg, keyVimRight):
			wm.moveRight()
		// search key '/'
		case key.Matches(msg, keySearch):
			wm.searchMode = true
			wm.searchId++
			wm.search = ""
		case key.Matches(msg, keyPreview):
			wm.previewMode = !wm.previewMode
			wm.positions = make(map[string]position)
			filename, ok := wm.fileName()
			if !ok {
				return wm, nil
			}
			wm.prevName = filename
			wm.findPrevName = true

			if wm.previewMode {
				return wm, tea.EnterAltScreen
			} else {
				wm.previewContent = ""
				return wm, tea.ExitAltScreen
			}

		case key.Matches(msg, keyDelete, keyFnDelete):
			filepathToDelete, ok := wm.filePath()
			if ok {
				if wm.deleteCurrentFile {
					wm.deleteCurrentFile = false
					wm.toBeDeleted = append(wm.toBeDeleted, toDelete{
						path: filepathToDelete,
						at:   time.Now().Add(6 * time.Second),
					})

					wm.list()
					wm.previewContent = ""
					return wm, tea.Tick(time.Second, func(time.Time) tea.Msg {
						return toBeDeleteMsg(0)
					})
				} else {
					wm.deleteCurrentFile = true
				}
			}

			return wm, nil
		case key.Matches(msg, keyYank):
			clipboard.WriteAll(wm.path)
			wm.yankSuccess = true
			return wm, nil
		case key.Matches(msg, keyHidden):
			wm.hideHidden = !wm.hideHidden
			wm.list()
		} // Internal switch

		wm.deleteCurrentFile = false
		wm.yankSuccess = false
		wm.updateOffset()
		wm.saveCursorPosition()
	// ------------------------------------------------ case 3
	case clearSearchMsg:
		if wm.searchId == int(msg) {
			wm.search = ""
			wm.searchMode = false
		}

	// -------------------------------------------------- case 4
	case toBeDeleteMsg:
		toBeDeleted := make([]toDelete, 0)
		for _, td := range wm.toBeDeleted {
			if td.at.After(time.Now()) {
				toBeDeleted = append(toBeDeleted, td)
			} else {
				remove(td.path)
			}
		}
		wm.toBeDeleted = toBeDeleted
		if len(wm.toBeDeleted) > 0 {
			return wm, tea.Tick(time.Second, func(time.Time) tea.Msg {
				return toBeDeleteMsg(0)
			})
		}
	}

	return wm, nil
}

func (wm *walker_model) fileName() (string, bool) {
	i := wm.c*wm.rows + wm.r

	if i >= len(wm.files) || i < 0 {
		return "", false
	}
	return wm.files[i].Name(), true
}

func (wm *walker_model) filePath() (string, bool) {
	filename, ok := wm.fileName()
	if !ok {
		return filename, false
	}

	return path.Join(wm.path, filename), true
}

func (wm *walker_model) openEditor() tea.Cmd {
	filepath, ok := wm.filePath()
	if !ok {
		return nil
	}

	cmdline := strings.Split(lookup([]string{Editor_walker, Editor}, "less"), " ")

	cmdline = append(cmdline, filepath)

	execCmd := exec.Command(cmdline[0], cmdline[1:]...)

	return tea.ExecProcess(execCmd, func(err error) tea.Msg {
		// todos: add document
		return nil
	})
}

func (wm *walker_model) saveCursorPosition() {
	wm.positions[wm.path] = position{
		col:    wm.c,
		row:    wm.r,
		offset: wm.offset,
	}
}

func (wm *walker_model) listHeight() int {
	h := wm.height - 1 // minus 1 for location bar.
	if len(wm.toBeDeleted) > 0 {
		h-- // minus 1 for delete bar
	}
	return h
}

func (wm *walker_model) updateOffset() {
	height := wm.listHeight()
	// scrolling down
	if wm.r >= wm.offset+height {
		wm.offset = wm.r - height + 1
	}
	// scrolling up
	if wm.r < wm.offset {
		wm.offset = wm.r
	}
	// don't scroll more than there are rows
	if wm.offset >= wm.rows-height && wm.rows > height {
		wm.offset = wm.rows - height
	}
	if wm.offset < 0 {
		wm.offset = 0
	}
}

func (wm *walker_model) list() {
	var err error
	wm.files = nil

	// ReadDir() already returns files and dirs sorted by filename
	files, err := os.ReadDir(wm.path)
	if err != nil {
		wm.err = err
		return
	} else {
		wm.err = nil
	}

_LABEL_FILES:
	for _, file := range files {
		if wm.hideHidden && strings.HasPrefix(file.Name(), ".") {
			continue _LABEL_FILES
		}

		if dirOnly && !file.IsDir() {
			continue _LABEL_FILES
		}

		for _, toDelete := range wm.toBeDeleted {
			if path.Join(wm.path, file.Name()) == toDelete.path {
				continue _LABEL_FILES
			}
		}

		wm.files = append(wm.files, file)
	}
}

// --------------------------------------------------- a lot of move methods
func (wm *walker_model) moveUp() {
	wm.r--
	if wm.r < 0 {
		wm.r = wm.rows - 1
		wm.c--
	}

	if wm.c < 0 {
		wm.r = wm.rows - 1 - (wm.columns*wm.rows - len(wm.files))
		wm.c = wm.columns - 1
	}
}

func (wm *walker_model) moveDown() {
	wm.r++
	if wm.r >= wm.rows {
		wm.r = 0
		wm.c++
	}

	if wm.c >= wm.columns {
		wm.c = 0
	}

	if wm.c == wm.columns-1 && (wm.columns-1)*wm.rows+wm.r >= len(wm.files) {
		wm.r = 0
		wm.c = 0
	}
}

func (wm *walker_model) moveLeft() {
	wm.c--
	if wm.c < 0 {
		wm.c = wm.columns - 1
	}
	if wm.c == wm.columns-1 && (wm.columns-1)*wm.rows+wm.r >= len(wm.files) {
		wm.r = wm.rows - 1 - (wm.columns*wm.rows - len(wm.files))
		wm.c = wm.columns - 1
	}
}

func (wm *walker_model) moveRight() {
	wm.c++
	if wm.c >= wm.columns {
		wm.c = 0
	}

	if wm.c == wm.columns-1 && (wm.columns-1)*wm.rows+wm.r >= len(wm.files) {
		wm.r = wm.rows - 1 - (wm.columns*wm.rows - len(wm.files))
		wm.c = wm.columns - 1
	}
}

func (wm *walker_model) moveTop() {
	wm.r = 0
}

func (wm *walker_model) moveBottom() {
	wm.r = wm.rows - 1
	if wm.c == wm.columns-1 && (wm.columns-1)*wm.rows+wm.r >= len(wm.files) {
		wm.r = wm.rows - 1 - (wm.columns*wm.rows - len(wm.files))
	}
}

func (wm *walker_model) moveLeftmost() {
	wm.c = 0
}

func (wm *walker_model) moveRightmost() {
	wm.c = wm.columns - 1
	if (wm.columns-1)*wm.rows+wm.r >= len(wm.files) {
		wm.r = wm.rows - 1 - (wm.columns*wm.rows - len(wm.files))
	}
}

func (wm *walker_model) moveStart() {
	wm.moveLeftmost()
	wm.moveTop()
}

func (wm *walker_model) moveEnd() {
	wm.moveRightmost()
	wm.moveBottom()
}

// --------------------------------------------------------------------- end move methods

// --------------------------------------------------------------------- for delete
func (wm *walker_model) dontDoPendingDeletions() {
	for _, toDelete := range wm.toBeDeleted {
		fmt.Fprintf(os.Stderr, "Was not deleted: %v\n", toDelete.path)
	}
}

func (wm *walker_model) performPendingDeletions() {
	for _, toDelete := range wm.toBeDeleted {
		remove(toDelete.path)
	}
	wm.toBeDeleted = nil
}

// ------------------------------------------------------------------ end for delete
func (wm *walker_model) preview() {
	if !wm.previewMode {
		return
	}

	filepath, ok := wm.filePath()
	if !ok {
		return
	}

	fileinfo, err := os.Stat(filepath)
	if err != nil {
		return
	}

	width := wm.width / 2
	height := wm.height - 1 // minus 1 for name bar

	if fileinfo.IsDir() {
		files, err := os.ReadDir(filepath)
		if err != nil {
			wm.previewContent = err.Error()
		}

		names, rows, cols := wrap(files, width, height, nil)

		output := make([]string, rows)
		for j := 0; j < rows; j++ {
			row := make([]string, cols)
			for i := 0; i < cols; i++ {
				row[i] = names[i][j]
			}

			output[j] = strings.Join(row, SEPARATOR)
		}

		if len(output) >= height {
			output = output[0:height]
		}

		wm.previewContent = strings.Join(output, "\n")
		return
	}

	if isImageExt(filepath) {
		img, err := drawImage(filepath, width, height)
		if err != nil {
			wm.previewContent = warning.Render("No image preview available")
			return
		}
		wm.previewContent = img
		return
	}

	var content []byte
	// if file is too big ( more than 100kb), read only first 100kb
	if fileinfo.Size() > 100*1024 {
		file, err := os.Open(filepath)
		if err != nil {
			wm.previewContent = err.Error()
			return
		}
		defer file.Close()

		content = make([]byte, 100*1024)
		_, err = file.Read(content)
		if err != nil {
			wm.previewContent = err.Error()
			return
		}
	} else {
		content, err = os.ReadFile(filepath)
		if err != nil {
			wm.previewContent = err.Error()
			return
		}
	}

	switch {
	case utf8.Valid(content):
		wm.previewContent = leaveOnlyAscii(content)
	default:
		wm.previewContent = warning.Render("No preview available")
	}
}

// MUST impl this View() interface for tea.model

func (wm *walker_model) View() string {
	width := wm.width
	if wm.previewMode {
		width = wm.width / 2
	}
	height := wm.listHeight()

	var names [][]string
	names, wm.rows, wm.columns = wrap(wm.files, width, height, func(name string, i, j int) {
		if wm.findPrevName && wm.prevName == name {
			wm.c = i
			wm.r = j
		}
	})

	if wm.findPrevName {
		wm.findPrevName = false
		wm.updateOffset()
		wm.saveCursorPosition()
	}

	wm.preview()

	outputWidth := strlen(path.Base(wm.path))
	if wm.previewMode {
		row := make([]string, wm.columns)
		for i := 0; i < wm.columns; i++ {
			if len(names[i]) > 0 {
				row[i] = names[i][0]
			} else {
				outputWidth = width
			}
		}
		outputWidth = max(outputWidth, strlen(strings.Join(row, SEPARATOR)))
	} else {
		outputWidth = width
	}

	output := make([]string, wm.rows)
	for j := 0; j < wm.rows; j++ {
		row := make([]string, wm.columns)

		for i := 0; i < wm.columns; i++ {
			if i == wm.c && j == wm.r {
				if wm.deleteCurrentFile {
					row[i] = danger.Render(names[i][j])
				} else {
					row[i] = cursor.Render(names[i][j])
				}
			} else {
				row[i] = names[i][j]
			}
		}

		output[j] = strings.Join(row, SEPARATOR)
	}

	if len(output) >= wm.offset+height {
		output = output[wm.offset : wm.offset+height]
	}

	filename, _ := wm.fileName()
	previewPane := bar.Render(filename) + "\n"
	previewPane += wm.previewContent
	// ------------------------------ location bar (Grep Color)
	location := wm.path
	if userHomeDir, err := os.UserHomeDir(); err == nil {
		location = strings.Replace(wm.path, userHomeDir, "~", 1)
	}
	// ------------------------------ windows OS ?
	if runtime.GOOS == "windows" {
		location = strings.ReplaceAll(
			strings.Replace(location, "\\/", fileSeparator, 1), "/", fileSeparator)
	}

	filter := ""
	if wm.searchMode || fuzzyByDefault {
		filter = fileSeparator + wm.search

		if fuzzyByDefault && wm.search == "" {
			filter = ""
		}
	}
	barLen := strlen(location) + strlen(filter)
	if barLen > outputWidth {
		location = location[min(barLen-outputWidth, strlen(location)):]
	}
	barStr := bar.Render(location) + search.Render(filter)

	main := barStr + "\n" + strings.Join(output, "\n")

	if wm.err != nil {
		main = barStr + "\n" + warning.Render(wm.err.Error())
	} else if len(wm.files) == 0 {
		main = barStr + "\n" + warning.Render("No files")
	}

	if len(wm.toBeDeleted) > 0 {
		toDelete := wm.toBeDeleted[len(wm.toBeDeleted)-1]
		// It is shorthand for t.Sub(time.Now()).
		timeLeft := int(toDelete.at.Sub(time.Now()).Seconds())
		deleteBar := fmt.Sprintf("%v deleted. (u)ndo %v", path.Base(toDelete.path), timeLeft)
		main += "\n" + danger.Render(deleteBar)
	}

	// Yank success.
	if wm.yankSuccess {
		yankBar := fmt.Sprintf("yanked path to clipboard: %v", wm.path)
		main += "\n" + bar.Render(yankBar)
	}

	if wm.previewMode {
		return lipgloss.JoinHorizontal(
			lipgloss.Top,
			main,
			preview.
				MaxHeight(wm.height).
				Render(previewPane),
		)
	} else {
		return main
	}
}

/*
ai check:
以下是对当前文件的代码解读：
这段代码是Go语言编写的，属于一个名为go-walker的项目，具体文件为src/walker_model.go。该文件定义了一个名为walker_model的结构体，以及一系列与之相关的方法，用于实现一个文件浏览器的功能。下面是对这段代码的详细解析：

概括性总结
walker_model结构体及其方法实现了一个基于文本的文件浏览器，支持浏览文件系统、搜索文件、预览文件内容、删除文件等功能。它使用tea库构建用户界面，处理键盘输入，以及管理文件浏览的状态。

结构体定义
position: 用于记录光标在文件列表中的位置。
toDelete: 表示待删除文件的信息。
walker_model: 主要的结构体，包含了文件浏览的各种状态和属性，如当前路径、文件列表、选择器位置、终端尺寸、搜索关键词等。
方法概述
Init(): 初始化函数。
updateSearch(msg tea.KeyMsg): 根据键盘输入更新搜索关键词。
Update(msg tea.Msg): 根据不同的消息更新模型状态。
fileName(), filePath(): 获取当前选中的文件名或路径。
openEditor(): 打开选中的文件。
saveCursorPosition(): 保存当前光标位置。
listHeight(), updateOffset(), list(): 管理文件列表的显示。
移动方法（如moveUp(), moveDown()等）: 控制光标在文件列表中的移动。
删除相关方法（如dontDoPendingDeletions(), performPendingDeletions()）: 处理文件的删除操作。
preview(): 预览选中文件的内容。
View(): 生成用于显示的字符串。
功能细节
支持通过键盘操作浏览文件系统，包括上下左右移动、页面滚动、打开文件或目录、返回上级目录等。
支持模糊搜索，快速定位文件。
可以预览文件内容，对于大文件或非文本文件有特别处理。
支持删除文件，并有撤销删除的功能。
支持隐藏或显示隐藏文件。
集成剪贴板功能，可以将当前路径复制到剪贴板。
界面布局会根据终端大小动态调整。
技术点
使用了tea库来构建命令行界面，处理用户输入和视图渲染。
利用Go的标准库如os, path, io/fs等进行文件系统操作。
使用fuzzy库实现模糊搜索功能。
界面美化使用了lipgloss库。
总结
这段代码是一个功能丰富的命令行文件浏览器的核心实现，展示了如何在Go语言中处理文件系统、用户输入、界面渲染等多方面的任务。通过这个项目，可以学习到如何在命令行环境下创建交互式应用程序。
*/
